/**
 * @file    processor.c
 * @author  Thomas Di'Meco and Matthieu Maugard
 * @brief   Execute instructions
 */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include "memory.h"
#include "processor.h"
#include "instructions.h"
#include "errors.h"


// Private members
int execute_load (int16_t mode, int16_t src, int16_t dest);
int execute_store(int16_t mode, int16_t src, int16_t dest);
int execute_add  (int16_t mode, int16_t src, int16_t dest);
int execute_sub  (int16_t mode, int16_t src, int16_t dest);
int execute_jmp  (int16_t mode, int16_t src, int16_t dest);
int execute_jeq  (int16_t mode, int16_t src, int16_t dest);
int execute_call (int16_t mode, int16_t src, int16_t dest);
int execute_ret  (int16_t mode, int16_t src, int16_t dest);
int execute_push (int16_t mode, int16_t src, int16_t dest);
int execute_pop  (int16_t mode, int16_t src, int16_t dest);
int execute_halt (int16_t mode, int16_t src, int16_t dest);
int16_t * get_src_zone (int16_t op, int16_t op_mode, int * err);
int16_t * get_dest_zone (int16_t op, int16_t op_mode, int * err);
void step_by_step_display (int16_t word, FILE * output_file, FILE * input_file);


/**
 * Execute the next instruction pointed by the PC register
 * @param step_by_step Boolean that indicate if the program
 *                     have to run step by step
 * @param output_file  The output stream for step by step display (to be more generic)
 * @param input_file   The input stream for step by step commands (to be more generic)
 * @return The error code:
 *         - error code = 0 means no error
 *         - error code < 0 means the program has been halted
 *         - error code > 0 means a specific error code
 */
int execute (int step_by_step, FILE * output_file, FILE * input_file)
{
    // Load the instruction pointed by PC
    if (pc+1 >= MEMORY_SIZE) return ERROR_REACHED_MEMORY_LIMIT;
    word_t word = mem[pc++];

    // Step by step display
    if (step_by_step) step_by_step_display(pc-1, output_file, input_file);

    int16_t mode = word.inst.mode;
    int16_t src  = word.inst.src;
    int16_t dest = word.inst.dest;

    int err = 0;

    // Execute the appropriate instruction
    switch (word.inst.codeop)
    {
        case LOAD:  err = execute_load (mode, src, dest); break;
        case STORE: err = execute_store(mode, src, dest); break;
        case ADD:   err = execute_add  (mode, src, dest); break;
        case SUB:   err = execute_sub  (mode, src, dest); break;
        case JMP:   err = execute_jmp  (mode, src, dest); break;
        case JEQ:   err = execute_jeq  (mode, src, dest); break;
        case CALL:  err = execute_call (mode, src, dest); break;
        case RET:   err = execute_ret  (mode, src, dest); break;
        case PUSH:  err = execute_push (mode, src, dest); break;
        case POP:   err = execute_pop  (mode, src, dest); break;
        case HALT:  err = execute_halt (mode, src, dest); break;
        default:    err = ERROR_UNKNOWN_INSTRUCTION;      break;
    }

    // Return the error code
    return err;
}


/**
 * Execute a load instruction
 * @param mode The instruction mode
 * @param src  The source register
 * @param dest The destination register
 * @return The error code:
 *         - error code = 0 means no error
 *         - error code < 0 means the program has been halted
 *         - error code > 0 means a specific error code
 */
int execute_load (int16_t mode, int16_t src, int16_t dest)
{
    // Get source and destination modes
    int16_t src_mode  = get_src_mode (mode);
    int16_t dest_mode = get_dest_mode(mode);

    // Check modes validity
    if (dest_mode != REG || src_mode == REG || src_mode == UNK)
        return ERROR_INVALID_MODE;

    int err;

    // Get source data and check errors
    int16_t * src_data = get_src_zone(src, src_mode, &err);
    if (err != NO_ERROR) return err;

    // Get destination zone and check errors
    int16_t * dest_zone = get_dest_zone(dest, dest_mode, &err);
    if (err != NO_ERROR) return err;

    // LOAD
    *dest_zone = *src_data;

    return NO_ERROR;
}


/**
 * Execute a store instruction
 * @param mode The instruction mode
 * @param src  The source register
 * @param dest The destination register
 * @return The error code:
 *         - error code = 0 means no error
 *         - error code < 0 means the program has been halted
 *         - error code > 0 means a specific error code
 */
int execute_store (int16_t mode, int16_t src, int16_t dest)
{
    // Get source and destination modes
    int16_t src_mode  = get_src_mode (mode);
    int16_t dest_mode = get_dest_mode(mode);

    // Check modes validity
    if ((dest_mode != DIR && dest_mode != IND) ||
        (src_mode != REG && src_mode != IMM))
        return ERROR_INVALID_MODE;

    int err;

    // Get source data and check errors
    int16_t * src_data = get_src_zone(src, src_mode, &err);
    if (err != NO_ERROR) return err;

    // Get destination zone and check errors
    int16_t * dest_zone = get_dest_zone(dest, dest_mode, &err);
    if (err != NO_ERROR) return err;

    // STORE
    *dest_zone = *src_data;

    return NO_ERROR;
}


/**
 * Execute an add instruction
 * @param mode The instruction mode
 * @param src  The source register
 * @param dest The destination register
 * @return The error code:
 *         - error code = 0 means no error
 *         - error code < 0 means the program has been halted
 *         - error code > 0 means a specific error code
 */
int execute_add (int16_t mode, int16_t src, int16_t dest)
{
    // Get source and destination modes
    int16_t src_mode  = get_src_mode (mode);
    int16_t dest_mode = get_dest_mode(mode);

    // Check modes validity
    if (dest_mode != REG || src_mode == UNK)
        return ERROR_INVALID_MODE;

    int err;

    // Get source data and check errors
    int16_t * src_data = get_src_zone(src, src_mode, &err);
    if (err != NO_ERROR) return err;

    // Get destination zone and check errors
    int16_t * dest_zone = get_dest_zone(dest, dest_mode, &err);
    if (err != NO_ERROR) return err;

    // ADD
    *dest_zone = *dest_zone + *src_data;
    sr = *dest_zone;

    return NO_ERROR;
}


/**
 * Execute a sub instruction
 * @param mode The instruction mode
 * @param src  The source register
 * @param dest The destination register
 * @return The error code:
 *         - error code = 0 means no error
 *         - error code < 0 means the program has been halted
 *         - error code > 0 means a specific error code
 */
int execute_sub (int16_t mode, int16_t src, int16_t dest)
{
    // Get source and destination modes
    int16_t src_mode  = get_src_mode (mode);
    int16_t dest_mode = get_dest_mode(mode);

    // Check modes validity
    if (dest_mode != REG || src_mode == UNK)
        return ERROR_INVALID_MODE;

    int err;

    // Get source data and check errors
    int16_t * src_data = get_src_zone(src, src_mode, &err);
    if (err != NO_ERROR) return err;

    // Get destination zone and check errors
    int16_t * dest_zone = get_dest_zone(dest, dest_mode, &err);
    if (err != NO_ERROR) return err;

    // SUB
    *dest_zone = *dest_zone - *src_data;
    sr = *dest_zone;

    return NO_ERROR;
}


/**
 * Execute a jmp instruction
 * @param mode The instruction mode
 * @param src  The source register
 * @param dest The destination register
 * @return The error code:
 *         - error code = 0 means no error
 *         - error code < 0 means the program has been halted
 *         - error code > 0 means a specific error code
 */
int execute_jmp (int16_t mode, int16_t src, int16_t dest)
{
    // Get source mode
    int16_t src_mode  = get_src_mode (mode);

    // Check mode validity
    if (src_mode != IMM)
        return ERROR_INVALID_MODE;

    int err;

    // Get source data and check errors
    int16_t * src_data = get_src_zone(src, src_mode, &err);
    if (err != NO_ERROR) return err;

    // JMP
    pc = *src_data;

    return NO_ERROR;
}


/**
 * Execute a jeq instruction
 * @param mode The instruction mode
 * @param src  The source register
 * @param dest The destination register
 * @return The error code:
 *         - error code = 0 means no error
 *         - error code < 0 means the program has been halted
 *         - error code > 0 means a specific error code
 */
int execute_jeq (int16_t mode, int16_t src, int16_t dest)
{
    // Get source mode
    int16_t src_mode  = get_src_mode (mode);

    // Check mode validity
    if (src_mode != IMM)
        return ERROR_INVALID_MODE;

    int err;

    // Get source data and check errors
    int16_t * src_data = get_src_zone(src, src_mode, &err);
    if (err != NO_ERROR) return err;

    // JEQ
    if (sr == 0) pc = *src_data;

    return NO_ERROR;
}


/**
 * Execute a call instruction
 * @param mode The instruction mode
 * @param src  The source register
 * @param dest The destination register
 * @return The error code:
 *         - error code = 0 means no error
 *         - error code < 0 means the program has been halted
 *         - error code > 0 means a specific error code
 */
int execute_call (int16_t mode, int16_t src, int16_t dest)
{
    // Get source mode
    int16_t src_mode  = get_src_mode (mode);

    // Check mode validity
    if (src_mode != IMM)
        return ERROR_INVALID_MODE;

    int err;

    // Get source data and check errors
    int16_t * src_data = get_src_zone(src, src_mode, &err);
    if (err != NO_ERROR) return err;

    // CALL
    if (!is_mem(sp)) return ERROR_INVALID_STACK_ADDRESS;
    mem[sp--].raw = pc;
    pc = *src_data;

    return NO_ERROR;
}


/**
 * Execute a ret instruction
 * @param mode The instruction mode
 * @param src  The source register
 * @param dest The destination register
 * @return The error code:
 *         - error code = 0 means no error
 *         - error code < 0 means the program has been halted
 *         - error code > 0 means a specific error code
 */
int execute_ret (int16_t mode, int16_t src, int16_t dest)
{
    // RET
    if (!is_mem(sp+1)) return ERROR_INVALID_STACK_ADDRESS;
    pc = mem[++sp].raw;

    return NO_ERROR;
}


/**
 * Execute a push instruction
 * @param mode The instruction mode
 * @param src  The source register
 * @param dest The destination register
 * @return The error code:
 *         - error code = 0 means no error
 *         - error code < 0 means the program has been halted
 *         - error code > 0 means a specific error code
 */
int execute_push (int16_t mode, int16_t src, int16_t dest)
{
    // Get source mode
    int16_t src_mode  = get_src_mode (mode);

    // Check mode validity
    if (src_mode == UNK)
        return ERROR_INVALID_MODE;

    int err;

    // Get source data and check errors
    int16_t * src_data = get_src_zone(src, src_mode, &err);
    if (err != NO_ERROR) return err;

    // PUSH
    if (!is_mem(sp)) return ERROR_INVALID_STACK_ADDRESS;
    mem[sp--].raw = *src_data;

    return NO_ERROR;
}


/**
 * Execute a pop instruction
 * @param mode The instruction mode
 * @param src  The source register
 * @param dest The destination register
 * @return The error code:
 *         - error code = 0 means no error
 *         - error code < 0 means the program has been halted
 *         - error code > 0 means a specific error code
 */
int execute_pop (int16_t mode, int16_t src, int16_t dest)
{
    // Get destination mode
    int16_t dest_mode = get_dest_mode(mode);

    // Check mode validity
    if (dest_mode == IMM || dest_mode == UNK)
        return ERROR_INVALID_MODE;

    int err;

    // Get destination zone and check errors
    int16_t * dest_zone = get_dest_zone(dest, dest_mode, &err);
    if (err != NO_ERROR) return err;

    // POP
    if (!is_mem(sp+1)) return ERROR_INVALID_STACK_ADDRESS;
    *dest_zone = mem[++sp].raw;

    return NO_ERROR;
}


/**
 * Execute a halt instruction
 * @param mode The instruction mode
 * @param src  The source register
 * @param dest The destination register
 * @return The error code:
 *         - error code = 0 means no error
 *         - error code < 0 means the program has been halted
 *         - error code > 0 means a specific error code
 */
int execute_halt (int16_t mode, int16_t src, int16_t dest)
{
    // HALT
    return -1; // Return error code < 0 to halt the program
}


/**
 * Return the source data zone (register or memory), depending on
 * the operand mode (register, immediate, direct, indirect).
 * This operation can return an error code in the err parameter
 * @param op      The operand
 * @param op_mode The operand mode
 * @param err     The error code returned
 * @return        The source data zone
 */
int16_t * get_src_zone (int16_t op, int16_t op_mode, int * err)
{
    *err = NO_ERROR;

    // Register operand mode
    if (op_mode == REG)
    {
        // Check register number
        if (!is_reg(op))
        {
            *err = ERROR_INVALID_REGISTER;
            return 0;
        }

        return &(reg[op]);
    }
    // Immediate operand mode
    else if (op_mode == IMM)
    {
        // Fetch next word
        if (pc+1 >= MEMORY_SIZE)
        {
            *err = ERROR_REACHED_MEMORY_LIMIT;
            return 0;
        }

        return &(mem[pc++].raw);
    }
    // Direct operand mode
    else if (op_mode == DIR)
    {
        // Fetch next word
        if (pc+1 >= MEMORY_SIZE)
        {
            *err = ERROR_REACHED_MEMORY_LIMIT;
            return 0;
        }

        int16_t adr = mem[pc++].raw;

        // Check memory address
        if (!is_mem(adr))
        {
            *err = ERROR_INVALID_MEMORY_ADDRESS;
            return 0;
        }

        return &(mem[adr].raw);
    }
    // Indirect operand mode
    else if (op_mode == IND)
    {
        // Check register number
        if (!is_reg(op))
        {
            *err = ERROR_INVALID_REGISTER;
            return 0;
        }

        int16_t adr = reg[op];

        // Check memory address
        if (!is_mem(adr))
        {
            *err = ERROR_INVALID_MEMORY_ADDRESS;
            return 0;
        }

        return &(mem[adr].raw);
    }

    // Unknown operand mode
    *err = ERROR_INVALID_MODE;
    return 0;
}


/**
 * Return the destination zone (register or memory), depending on
 * the operand mode (register, direct, indirect).
 * This operation can return an error code in the err parameter
 * @param op      The operand
 * @param op_mode The operand mode
 * @param err     The error code returned
 * @return        The destination zone
 */
int16_t * get_dest_zone (int16_t op, int16_t op_mode, int * err)
{
    // Immediate mode is forbidden for destination
    if (op_mode == IMM)
    {
        *err = ERROR_INVALID_MODE;
        return 0;
    }

    // Call get_src_zone function (same process)
    return get_src_zone (op, op_mode, err);
}


/**
 * Display the step by step instruction into the output stream, and
 * wait for the user from the input stream
 * @param word        The memory address of the word to display to the user
 * @param output_file The output stream for step by step display (to be more generic)
 * @param input_file  The input stream for step by step commands (to be more generic)
 */
void step_by_step_display (int16_t word, FILE * output_file, FILE * input_file)
{
    // Clear the screen
    if (getenv("windir") != NULL) system("clrscr");
    else system("clear");

    // Display message
    fprintf(output_file,
        "PROCSI step by step display\n"
        " - Press return to execute the current instruction or\n"
        " - Type a decimal memory address to display it\n"
        "Note: 'jump to instruction' addresses are converted to memory addresses\n"
        "\n"
    );

    // Dump registers
    dump_reg();

    // Display the word
    char inst[100];
    int err_code = mnemonic_instruction(word, inst, 100);
    if (err_code) strcpy(inst, "MALFORMED INSTRUCTION");
    fprintf(output_file, "\nNext: %s\n\n", inst);

    // Wait for user action
    for (;;)
    {
        // Show prompt
        fprintf(output_file, "> ");

        // Get user command
        char line[10];
        fgets(line, 10, input_file);
        fflush(input_file);

        // If the user wants to show a memory address
        if (line[0] != '\n' && line[0] != '\r')
        {
            int16_t adr = atoi(line);

            // Check address validity
            if (is_mem(adr))
                fprintf(output_file, "%04x: %02X %02X\n",
                        adr, mem[adr].raw >> 8 & 0x00FF, mem[adr].raw & 0x00FF);
            else
                fprintf(output_file, "Invalid memory address\n");
        }
        else break;
    }
}
